package com.gy.boot.common.jwt;

import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import cn.hutool.core.net.NetUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.gy.boot.common.dao.IEntity;
import com.gy.boot.common.exception.SysException;
import com.gy.boot.common.util.BlankUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.LinkedHashSet;

/**
 * JWT过滤器
 * */
@Component
public class JWTInterceptor extends HandlerInterceptorAdapter {
    private static Logger logger = LoggerFactory.getLogger(JWTInterceptor.class);

    // 根据IP设置访问频率设置; 可通过缓存(默认过期时间10秒) IP_servletPath 的请求数; 超出最大值后,阻止访问;
    private static final int IP_TIMED_MAX_COUNT = 10; // 每10秒最大访问10次
    private static final TimedCache<String, Integer> IP_TIMED_CACHE = CacheUtil.newTimedCache(10000);
    static {
        IP_TIMED_CACHE.schedulePrune(100); // 每0.1秒检查一次过期
    }
    /** 校验IP访问频率 */
    private static void checkIPFrequency(String clientIP, String servletPath) {
        // 1. 同一IP访问的频率
        Integer ipCount = IP_TIMED_CACHE.get(clientIP);
        if (ipCount == null) {
            IP_TIMED_CACHE.put(clientIP, 1);
        } else {
            ipCount = ipCount + 1;
            IP_TIMED_CACHE.put(clientIP, ipCount);
            if (ipCount > (IP_TIMED_MAX_COUNT*5)) {
                throw new SysException("您请求次数过多, 请待会访问", 402);
            }
        }
        // 2. 同一IP同一接口的访问频率
        String ipPath = clientIP + "-" + servletPath;
        Integer count = IP_TIMED_CACHE.get(ipPath, false);
        if (count == null) { // 第一次访问
            IP_TIMED_CACHE.put(ipPath, 1);
        } else {
            count = count + 1;
            IP_TIMED_CACHE.put(ipPath, count);
            if (count > IP_TIMED_MAX_COUNT) {
                throw new SysException("您请求频率过快, 请待会访问", 403);
            }
        }
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String servletPath = request.getServletPath();  // 访问地址
        String clientIP = ServletUtil.getClientIP(request); // 客户IP
        // TODO 此出可做点击统计 NoSql
        logger.info("clientIP: {}; servletPath: {};", clientIP, servletPath);
        // 相同IP 的频率校验
        checkIPFrequency(clientIP, servletPath);
        // 1.校验IP
        boolean isNotAccessIP = !isCanAccessIP(clientIP);
        if (isNotAccessIP) {
            throw new SysException("非法IP不能访问", 401);
        }
        // 不需要验证,直接放行
        boolean isNotCheck = isNotCheck(servletPath);
        if (isNotCheck) { return true; }
        // 需要验证, 获取签名信息
        String token = getToken(request);
        IEntity tokenE = JWT.checkToken(token);
        System.out.println("TOKEN: " + tokenE);
        // 将签名中获取的用户信息放入request中;
        request.setAttribute(JWT.USER_ID, tokenE.getLong(JWT.USER_ID));
        request.setAttribute(JWT.ROLE_ID, tokenE.getLong(JWT.ROLE_ID));
        // 是否有刷新Token
        String refreshToken = tokenE.getStr(JWT.REFRESH_TOKEN);
        if (!BlankUtil.isBlank(refreshToken)) {
            // 设置刷新后的SQL到客户端
            JWT.setToken(request, response, refreshToken);
        }
        return true;
    }
    /**
     * 获取请求Token
     */
    private String getToken(HttpServletRequest request) {
        String token = request.getHeader(JWT.TOKEN);
        if (BlankUtil.isBlank(token)) {
            token = request.getParameter(JWT.TOKEN);
        }
        if (BlankUtil.isBlank(token)) {
            Cookie cookieToken = ServletUtil.getCookie(request, JWT.TOKEN);
            if (cookieToken != null) {
                token = cookieToken.getValue();
            }
        }
        if (BlankUtil.isBlank(token)) {
            HttpSession session = request.getSession();
            if (session != null) {
                Object tokenObj = session.getAttribute(JWT.TOKEN);
                if (tokenObj != null) {
                    token = tokenObj.toString();
                }
            }
        }
        return token;
    }

    /** 不用拦截的页面路径(也可存入数据库中), 不要以 / 结尾 **/
    private static final String[] NOT_CHECK_URL = {"/error", "/logs/**"
            , "/files/**", "/html/**", "/ico/**", "/favicon.ico"
            , "/test/**", "/WebSocket/**", "/login/**", "/auth/**"
    };

    /**  根据URL判断当前请求是否不需要校验, true:不需要校验 **/
    private static boolean isNotCheck(String servletPath) {
        // 若 请求接口 以 / 结尾, 则去掉 /
        servletPath = servletPath.endsWith("/")
                ? servletPath.substring(0, servletPath.lastIndexOf("/")) : servletPath;
        for (String path : NOT_CHECK_URL) {
            if (path.equals(servletPath)) { return true; }
            // path 以 /** 结尾, servletPath 以 path 前缀开头:  path = "/test/**"
            if (path.endsWith("/**")) {
                int lastIndex = path.lastIndexOf("/");
                String pathStart1 = path.substring(0, lastIndex + 1); // pathStart1 = "/test/"
                if (servletPath.startsWith(pathStart1)) {
                    return true;
                }
                String pathStart2 = path.substring(0, lastIndex); //  pathStart2 = "/test"
                if (servletPath.equals(pathStart2)) {
                    return true;
                }
            }
        }
        return false;
    }

    // 设置IP白名单
    private static final String[] NOT_CHECK_IP = {"127.0.0.1", "0:0:0:0:0:0:0:1"};

    /**
     * 判断IP是否可访问
     * @param clientIP 客户端IP
     * */
    private boolean isCanAccessIP(String clientIP) {
        // 判断是否白名单IP访问
        for (String ip : NOT_CHECK_IP) {
            if (ip.equals(clientIP)) {
                return true;
            }
        }
        // 判断是否本地IP访问
        // 获取当前系统所有IP
        LinkedHashSet<String> localIPs = NetUtil.localIpv4s();
        if (!localIPs.isEmpty()) {
            logger.info("=== 当前系统IP4:[ " + localIPs + " ]===");
            return localIPs.contains(clientIP);
        } else {
            return false;
        }
    }

}
